package edu.mcw.rgd.clinminer.domain;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.PostPersist;
import javax.persistence.PreRemove;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;

import org.apache.log4j.Logger;
import org.eclipse.persistence.annotations.Index;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord;
import org.springframework.roo.addon.tostring.RooToString;

import edu.mcw.rgd.clinminer.domain.common.Persistable;
import edu.mcw.rgd.clinminer.domain.enums.Gender;
import edu.mcw.rgd.clinminer.domain.misc.UserProfile;

@RooJavaBean
@RooToString
@RooJpaActiveRecord(persistenceUnit = "persistenceUnit")
@Table(name = "PARTICIPANT")
public class Participant implements Persistable {
	private static final Logger log = Logger.getLogger(Participant.class);

	@Id
	@GeneratedValue(generator = "Participant_IDs_Seq")
	@SequenceGenerator(name = "Participant_IDs_Seq", sequenceName = "Participant_IDs_Seq", allocationSize = 1)
	@Column(name = "participant_id")
	private Long id;

	@NotNull(message = "Name cannot be empty")
	@Column(name = "name", unique = true)
	@Index()
	private String name;

	@ManyToMany(mappedBy = "participants")
	@JoinTable(name = "study_participants", joinColumns = @JoinColumn(name = "participants"), inverseJoinColumns = @JoinColumn(name = "studies"))
	private Set<Study> studies = new HashSet<Study>();

	@OneToMany(mappedBy = "participant", cascade = CascadeType.ALL)
	private final Set<Phenotype> phenotypes = new HashSet<Phenotype>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "participant")
	private final Set<ImagingPhenotype> imagingPhenotypes = new HashSet<ImagingPhenotype>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "participant")
	private final Set<NormalPhenotype> normalPhenotypes = new HashSet<NormalPhenotype>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "participant")
	private final Set<Measurement> measurements = new HashSet<Measurement>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "participant")
	private final Set<GeneticLab> geneticLabs = new HashSet<GeneticLab>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "participant")
	private final Set<Medication> medications = new HashSet<Medication>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "participant")
	private final Set<Procedure> procedures = new HashSet<Procedure>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "source")
	private final Set<Relationship> relationshipSources = new HashSet<Relationship>();

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "target")
	private final Set<Relationship> relationshipTargets = new HashSet<Relationship>();

	@OneToMany(mappedBy = "defaultParticipant")
	private final Set<UserProfile> defaultInProfiles = new HashSet<UserProfile>();

	// Participant Details
	@NotNull(message = "First name cannot be empty")
	@Column(name = "first_name")
	private String firstName;

	@NotNull(message = "Last name cannot be empty")
	@Column(name = "last_name")
	private String lastName;

	@Enumerated(EnumType.STRING)
	@Column(name = "gender")
	private Gender gender;

	@Past(message = "Date of birth has to be in the past")
	@NotNull(message = "Date of birth cannot be empty")
	@Temporal(TemporalType.TIMESTAMP)
	@DateTimeFormat(style = "S-")
	@Column(name = "dob")
	private Date DOB;

	@Column(name = "mrn")
	private String MRN;

	@Column(name = "ssn")
	private String SSN;

	/**
	 * IMPORTANT: cleans up references from associated objects to avoid
	 * ConstraintViolationExceptions
	 */
	@PreRemove
	private void preRemove() {
		for (UserProfile up : defaultInProfiles) {
			up.setDefaultParticipant(null);
		}
		// Fetch indirect sets for cascade delete
		relationshipSources.size();
		relationshipTargets.size();

		for (Study s : studies) {
			s.getParticipants().remove(this);
			s.merge();
		}
	}

	public void setStudies(Set<Study> studies) {
		// calculate asymmetric set differences non-destructively
		Set<Study> removedStudies = new HashSet<Study>(this.studies);
		removedStudies.removeAll(studies);
		Set<Study> addedStudies = new HashSet<Study>(studies);
		addedStudies.removeAll(this.studies);

		this.studies = studies;

		// need to maintain bidirectional manually
		for (Study s : addedStudies) {
			s.getParticipants().add(this);
			s.merge();
		}

		for (Study s : removedStudies) {
			s.getParticipants().remove(this);
			s.merge();
		}
	}

	@PostPersist
	// only adding here
	private void syncBidirectionalPostPersist() {
		for (Study s : studies) {
			s.getParticipants().add(this);
			s.merge();
		}
	}

	@Override
	@Transient
	public String toString() {
		return getName();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		Participant other = (Participant) obj;
		if (id == null) {
			if (other.id != null) {
				return false;
			}
		} else if (!id.equals(other.id)) {
			return false;
		}
		return true;
	}

}
